﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Serialization;
using MinecraftProtocol.Packet;
using MinecraftProtocol.GameState;

namespace MinecraftProtocol
{
    public class MinecraftProtocolDecoder : TcpProxy.ProtocolDecoder
    {
        static Dictionary<int, McPacket> _packets;

        private GameState.GameState gameState { get { return (GameState.GameState)SessionState; } }

        bool _isServer;

        #region Ctor
        public MinecraftProtocolDecoder(bool isServer)
        {
            _isServer = isServer;

            cachePackets();
        }

        private void cachePackets()
        {
            if (_packets != null) return;

            //load the packet def in
            Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("MinecraftProtocol.packets.xml");
            byte[] buffer = new byte[stream.Length];
            stream.Read(buffer, 0, (int)stream.Length);

            XmlSerializer serializer = new XmlSerializer(typeof(List<McPacket>));
            List<McPacket> tempPackets = (List<McPacket>)serializer.Deserialize(new MemoryStream(buffer));

            _packets = new Dictionary<int, McPacket>();
            foreach (McPacket packet in tempPackets)
            {
                switch (packet.PacketDirection)
                {
                    case McPacketDirection.ClientToServer:
                        _packets.Add(packet.PacketId, packet);
                        break;

                    case McPacketDirection.ServerToClient:
                        _packets.Add(packet.PacketId + 1000, packet);
                        break;

                    case McPacketDirection.TwoWay:
                        _packets.Add(packet.PacketId, packet);
                        _packets.Add(packet.PacketId + 1000, packet);
                        break;
                }
            }
        }
        #endregion

        #region Decoder Main
        public override void DecoderMain()
        {
            try
            {
                byte lastCommand = 0;

                while (!ShouldExit)
                {
                    byte command = PacketStream.ReadByte();

                    int commandCode = _isServer ? command + 1000 : command;

                    //Console.WriteLine(string.Format("{0} command {1} at {2}", _isServer ? "Server" : "Client", command, PacketStream.Offset - 1));

                    if (!_packets.ContainsKey(commandCode))
                        throw new Exception("Unsupported command");

                    McPacket packet = _packets[commandCode];

                    Dictionary<string, object> properties = new Dictionary<string, object>();

                    foreach (McPacketField field in packet.Fields)
                    {
                        object value = null;

                        switch (field.FieldType)
                        {
                            case "byte":
                                value = PacketStream.ReadByte();
                                break;

                            case "bool":
                                value = (PacketStream.ReadByte() == 1);
                                break;

                            case "short":
                                value = PacketStream.ReadShort();
                                break;

                            case "int":
                                value = PacketStream.ReadInt();
                                break;

                            case "float":
                                value = PacketStream.ReadSingle();
                                break;

                            case "long":
                                value = PacketStream.ReadLong();
                                break;

                            case "double":
                                value = PacketStream.ReadDouble();
                                break;

                            case "string":
                                value = readString();
                                break;

                            case "metadata":
                                readMetaData();
                                break;

                            case "slot":
                                readSlot();
                                break;

                            case "slotarray":
                                for (int i = 0; i < (short)properties["Count"]; i++)
                                    readSlot();
                                break;

                            case "bytearray":
                                value = readByteArray(command, properties);
                                break;

                            default:
                                throw new Exception("Unsupported field type!");
                        }

                        if (!properties.ContainsKey(field.FieldName)) //don't add duplicates (eg. 'not used' fields)
                            properties.Add(field.FieldName, value);
                    }

                    handleCommand(command, properties);

                    //used for debugging. when packets are out of sync it's usually due to the last packet being interpreted incorrectly
                    lastCommand = command;
                }
            }
            catch
            {
                if (_isServer)
                    gameState.ServerDataBroken = true;
                else
                    gameState.ClientDataBroken = true;
            }
            finally
            {
                base.IsActive = false;
            }
        }
        #endregion

        #region Complex Type Handlers
        private string readString()
        {
            ushort len = (ushort)PacketStream.ReadShort();
            string s = System.Text.Encoding.BigEndianUnicode.GetString(PacketStream.ReadBytes(len * 2));
            return s;
        }

        private void readMetaData()
        {
            byte[] buffer;

            byte x = PacketStream.ReadByte();

            while (x != 127)
            {
                byte type = (byte)(x >> 5);

                switch (type)
                {
                    case 0:
                        PacketStream.ReadByte();
                        break;

                    case 1:
                        PacketStream.ReadShort();
                        break;

                    case 2:
                        PacketStream.ReadInt();
                        break;

                    case 3:
                        PacketStream.ReadSingle();
                        break;

                    case 4: //read string
                        readString();
                        break;

                    case 5:
                        buffer = PacketStream.ReadBytes(2 + 1 + 2);
                        break;

                    case 6:
                        buffer = PacketStream.ReadBytes(4 + 4 + 4);
                        break;

                    default:
                        throw new Exception("Unexpected code in metadata!");
                }

                x = PacketStream.ReadByte();
            }
        }

        private void readSlot()
        {
            short itemBlockId = PacketStream.ReadShort();

            if (itemBlockId == -1) return;

            byte count = PacketStream.ReadByte();

            short damageBlockMeta = PacketStream.ReadShort();

            if (isEnchantable(itemBlockId))
            {
                short compressedSize = PacketStream.ReadShort();
                if (compressedSize != -1)
                {
                    byte[] buffer = PacketStream.ReadBytes(compressedSize);
                }
            }
        }

        private bool isEnchantable(short value)
        {
            return (256 <= value && value <= 259) ||
                    (267 <= value && value <= 279) ||
                    (283 <= value && value <= 286) ||
                    (290 <= value && value <= 294) ||
                    (298 <= value && value <= 317) ||
                    value == 261 || value == 359 ||
                    value == 346;
        }

        private object readByteArray(byte command, Dictionary<string, object> properties)
        {
            object value = null;

            //byte array interpretation depends on the command
            switch (command)
            {
                case 0x33: //map chunks
                    //get compressed data
                    value = PacketStream.ReadBytes((int)properties["Compressed size"]);
                    break;

                case 0x34:
                    PacketStream.ReadBytes((int)properties["Data size"]);
                    break;

                case 0x3c: //explosion
                    PacketStream.ReadBytes((int)properties["Record count"] * 3);
                    break;

                case 0x83: //item data
                    PacketStream.ReadBytes((byte)properties["Text length"]);
                    break;

                case 0xfa://plugin message
                    PacketStream.ReadBytes((short)properties["length"]);
                    break;

                default:
                    throw new Exception("Unsupported packet with byte array type!");
            }

            return value;
        }
        #endregion

        #region Command Handlers
        /// <summary>
        /// The commands we're interested in for the game state are parsed here
        /// </summary>
        /// <param name="command"></param>
        /// <param name="properties"></param>
        private void handleCommand(byte command, Dictionary<string, object> properties)
        {
            switch (command)
            {
                case 0x06:
                    break;

                case 0x33: //map chunks
                    handleMapChunks(properties);
                    break;

                case 0x0B: //player position
                case 0x0C: //player look
                case 0x0D: //player position and look
                    handlePlayer(properties);
                    break;

                case 0x14: //spawn named entity (oither players)
                //case 0x17: //spawn object
                case 0x18: //spawn mob
                case 0x1f: //entity relative mode
                case 0x21: //entity look and relative move
                case 0x22: //entity teleport
                    handleEntity(properties);
                    break;

                case 0xff: //kick
                    handleKick(properties);
                    break;
            }
        }

        private void handleMapChunks(Dictionary<string, object> properties)
        {
            return;

            //decompress
            ICSharpCode.SharpZipLib.Zip.Compression.Inflater f = new ICSharpCode.SharpZipLib.Zip.Compression.Inflater(false);

            f.SetInput((byte[])properties["Compressed data"]);

            byte[] decompressedBuffer = new byte[5000000];
            int ret = f.Inflate(decompressedBuffer);

            if (ret == decompressedBuffer.Length)
                throw new Exception("Partial decompression!");

            Array.Resize(ref decompressedBuffer, ret);

            //map
            int offset = 0;

            int bitmask = (short)properties["Primary bit map"];

            int chunk_x = (int)properties["X"];
            int chunk_z = (int)properties["Z"];

            lock (gameState.Map)
            {
                //Loop over 16x16x16 chunks in the 16x256x16 column
                for (int i = 0; i < 16; i++)
                {
                    if ((bitmask & (1 << i)) != 0)
                    {
                        byte[] cubic_chunk_data = new byte[4096];
                        Array.Copy(decompressedBuffer, offset, cubic_chunk_data, 0, 4096);
                        offset += 4096;

                        for (int j = 0; j < cubic_chunk_data.Length; j++)
                        {
                            int x, y, z;
                            byte data;

                            //Byte arrays
                            x = chunk_x * 16 + (j & 0x0F);
                            y = i * 16 + (j >> 8);
                            z = chunk_z * 16 + ((j & 0xF0) >> 4);

                            data = cubic_chunk_data[j];

                            Point3D point = new Point3D(x, y, z);

                            if (gameState.Map.ContainsKey(point))
                                gameState.Map[point] = data;
                            else
                                gameState.Map.Add(point, data);
                        }

                    }
                }
            }
        }

        private void handlePlayer(Dictionary<string, object> properties)
        {
            if (properties.ContainsKey("X")) gameState.Player.Position.X = (double)properties["X"];
            if (properties.ContainsKey("Y")) gameState.Player.Position.Y = (double)properties["Y"];
            if (properties.ContainsKey("Z")) gameState.Player.Position.Z = (double)properties["Z"];

            if (properties.ContainsKey("Yaw")) gameState.Player.View.Yaw = (float)properties["Yaw"];
            if (properties.ContainsKey("Pitch")) gameState.Player.View.Pitch = (float)properties["Pitch"];
        }

        private void handleEntity(Dictionary<string, object> properties)
        {
            int eid = (int)properties["EID"];

            GameState.Entity entity = null;

            lock (gameState.Entities)
            {
                if (!gameState.Entities.ContainsKey(eid))
                {
                    entity = new GameState.Entity();
                    gameState.Entities.TryAdd(eid, entity);
                }
                else
                {
                    entity = gameState.Entities[eid];
                }
            }

            if (properties.ContainsKey("Player Name"))
                entity.Name = (string)properties["Player Name"];

            if (properties.ContainsKey("Type")) //mobs only, will clash with vehicles
                entity.Name = ((Types.MobTypes)(byte)properties["Type"]).ToString();

            if (properties.ContainsKey("dX")) //relative position
            {
                entity.Position.X += ((double)(sbyte)(byte)properties["dX"]) / 32.00;
                entity.Position.Y += ((double)(sbyte)(byte)properties["dY"]) / 32.00;
                entity.Position.Z += ((double)(sbyte)(byte)properties["dZ"]) / 32.00;
            }
            else if (properties.ContainsKey("X")) //absolute position
            {
                entity.Position.X = ((double)(int)properties["X"])/32.00;
                entity.Position.Y = ((double)(int)properties["Y"])/32.00;
                entity.Position.Z = ((double)(int)properties["Z"])/32.00;
            }

            if (properties.ContainsKey("Yaw"))//view
            {

            }

        }

        private void handleKick(Dictionary<string, object> properties)
        {
            TcpProxy.CloseTcpClients();
        }

        #endregion
    }
}
